# Copyright (C) 2007 Insecure.Com LLC.
#
# Authors: Guilherme Polo <ggpolo@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 
# USA

import gtk
import gobject
import cairo
import ConfigParser

from Base import TL_Base
from Base import BadConfFile
from Base import conf_file
from Base import tl_colors
from Base import too_dark
from Events import TL_Events

from EventPopup import TL_EventPopup # TEST!


####
## TimeLine

def font_size(font_file=conf_file):
    """
    Read and return font size from file.
    """
    cfg = ConfigParser.ConfigParser()
    cfg.read(font_file)

    if not cfg.has_option("Font", "min_font_size"):
        raise BadConfFile(font_file)
    
    return cfg.getint("Font", "min_font_size")


class TL(gtk.Widget, TL_Base, TL_Events):
    """
    TimeLine widget for displaying changes over time in a nice way.
    """
    
    __gsignals__ = {
                     'date-change-notify': (gobject.SIGNAL_RUN_LAST,
                                            gobject.TYPE_NONE,
                                            ()),
                     'range-change-notify': (gobject.SIGNAL_RUN_LAST,
                                             gobject.TYPE_NONE,
                                             ()),
                     'font-change-notify': (gobject.SIGNAL_RUN_LAST,
                                            gobject.TYPE_NONE,
                                            ()),
                     'color-change-notify': (gobject.SIGNAL_RUN_LAST,
                                             gobject.TYPE_NONE,
                                             ())
    }
    
    
    def __init__(self, zoom=0, start_year=2007, start_month=3,
                 start_day=15, start_hour=23, fwday=0, 
                 year_range=(1970, 3000), fixed_height=True,
                 border_on_focus=True, cell_border=True,
                 draw_back_shadow=True, light_on_hover=True,
                 show_every_date=False, evt_popup=True, testing_mode=False):
        """
        Params:
        
        zoom                  - Initial zoom level (0..3)
        
        start_year,
        start_month,
        start_day,
        start_hour          - Initial date
        
        fwday                 - First weekday (0 - monday, 6 - sunday)
        
        year_range        - Initial range used in TimeLine
        
        fixed_height       - Set cells to resize height on window resize, or not
        
        border_on_focus  - Draw a border around the widget when it receives
                          focus
        
        cell_border         - Draw a border around a cell
        
        draw_back_shadow - Draw shadow on cell
        
        light_on_hover   - Draw a light effect when a cell is being hovered
        
        show_every_date  - Show every date number or just highlighted dates.
        
        evt_popup        - When clicking in a date, popups a window or draw
                          something somewhere (not working for now)
        
        testing_mode     - Get random colors for dates, otherwise, will
                          get from some event gather (not working for now,
                          if you set this to False, every date will get a
                          "no_change" color)
        """
        
        gtk.Widget.__init__(self)
        TL_Base.__init__(self, zoom, start_year, start_month, start_day,
                         start_hour, fwday, year_range, border_on_focus)
        TL_Events.__init__(self, testing_mode)
        
        self.__focus = False
        
        self.show_every_date = show_every_date        
        self.evt_popup = evt_popup
        self.cell_border = cell_border
        self.light_on_hover = light_on_hover
        self.draw_backshadow = draw_back_shadow
        self.fixed_height = fixed_height
        self.min_font_size = font_size()
        self.min_cell_width = 0
        self.min_cell_height = 0
        self.hovering = -1
        
        self.connect('color-change-notify', lambda c: self.queue_draw())
        self.connect('font-change-notify', lambda f: self.__min_size())
        self.connect('date-change-notify', self.__update_date)
        self.connect('range-change-notify', self.__update_date)


    def get_show_all_dates(self):
        """
        Returns True if every date should be shown, otherwise, returns
        False.
        """
        return self.__show_all

    
    def set_show_all_dates(self, show_all):
        """
        Set to show all dates in TimeLine or not.
        """
        self.__show_all = show_all


    def get_evt_popup(self):
        """
        Returns if changes for a date should appear in a popup window
        or should be drawn somewhere.
        """
        return self.__evtpop


    def set_evt_popup(self, popup):
        """
        Sets how the changes for the date will appear, in a popup or drawn
        somewhere.
        """
        self.__evtpop = popup


    def get_height(self):
        """
        Returns cell height.
        """
        return (self.fixed_height and self.min_cell_height or \
                self.allocation[3])


    def get_fixed_height(self):
        """
        Return True if cell height is fixed, otherwise, False..
        """
        return self.__fixheight
    
    
    def set_fixed_height(self, fixed):
        """
        Enable/Disable cell fixed height.
        """
        self.__fixheight = fixed


    def get_hover(self):
        """
        Get hover state.
        """
        return self.__hover


    def get_font_size(self):
        """
        Get font size for text highlight
        """
        return font_size()


    def set_font_size(self, size):
        """
        Set font size for text highlight
        """
        cfg = ConfigParser.ConfigParser()
        cfg.read(conf_file)
        
        if not cfg.has_option("Font", "min_font_size"):
            raise BadConfFile(font_file)
        
        cfg.set("Font", "min_font_size", size)
        cfg.write(open(conf_file, 'w'))
        
    
    def get_cell_border(self):
        """
        Returns True is a border should be draw around a cell, otherwise,
        False. 
        """
        return self.__cborder
    
    
    def set_cell_border(self, draw_border):
        """
        Sets cell border to be drawn or not.
        """    
        self.__cborder = draw_border

    
    def get_light_hover(self):
        """
        Returns True if a light should be drawn when hovering a cell.
        """
        return self.__sshdw
        
        
    def set_light_hover(self, draw_shadow):
        """
        Sets cell light hover.
        """
        self.__sshdw = draw_shadow 
        
    
    def get_draw_bshadow(self):
        """
        Returns True if a back shadow should be drawn
        """
        return self.__bshdw
    
    
    def set_draw_bshadow(self, draw_shadow):
        """
        Set draw back shadow
        """
        self.__bshdw = draw_shadow
        
    
    def center_text(self, cr, text, cell_width):
        """
        Center text on x axis for text on a cell.
        """
        x_bearing, y_bearing, width, height, \
        x_advance, y_advance = cr.text_extents("%s" % text)
        
        return (cell_width/2.0 - (width/2.0 + x_bearing), self.height - 6)  
    
    
    def do_realize(self):
        self.set_flags(self.flags() | gtk.REALIZED | gtk.CAN_FOCUS)
        
        # create a new gtk.gdk.Window so we can play on it =)
        self.window = gtk.gdk.Window(self.get_parent_window(), 
                                     width=self.allocation.width,
                                     height=self.allocation.height, 
                                     window_type=gtk.gdk.WINDOW_CHILD,
                                     wclass=gtk.gdk.INPUT_OUTPUT,
                                     event_mask=self.get_events() | 
                                                gtk.gdk.EXPOSURE_MASK |
                                                gtk.gdk.BUTTON_PRESS_MASK | 
                                                gtk.gdk.BUTTON_RELEASE_MASK |
                                                gtk.gdk.POINTER_MOTION_MASK |
                                                gtk.gdk.ENTER_NOTIFY_MASK | 
                                                gtk.gdk.LEAVE_NOTIFY_MASK)
        
        # associate the gtk.gdk.Window with ourselves, Gtk+ needs a 
        # reference between the widget and the gdk window
        self.window.set_user_data(self)
        
        # attach the style to the gdk.Window, a style contains colors and
        # graphic contexts used for drawing                      
        self.style.attach(self.window)
        
        # the default color of the background should be what
        # theme engine tells us.
        self.style.set_background(self.window, gtk.STATE_NORMAL)
        
        self.window.move_resize(*self.allocation)
        
        # calculate size for three letters and resize window
        self.__min_size()
    
    
    def __min_size(self):
        cr = self.window.cairo_create()
        cr.set_font_size(self.min_font_size)
        _, _, width, height, _, _ = cr.text_extents("WWW")
        
        self.min_cell_width = width
        self.min_cell_height = (height * 5) + 2
        self.queue_resize()
    

    def do_unrealize(self):
        """
        Release resources when requested.
        """
        print ">> Leaving, current date:", self.get_date()
        self.window.set_user_data(None)
    

    def do_size_request(self, requisition):
        """
        Handle size request.
        """
        width, height = (self.min_cell_width + 2.5)**2, self.min_cell_height
        
        requisition.width = width
        requisition.height = height


    def do_size_allocate(self, allocation):
        """
        If we are realized, move and resize the window to the requested
        coords/positions, and set a maximum cell width for current
        window width. 
        """        
        self.allocation = allocation
        
        self.__dx = (allocation[2] - 4) / float(self.scale)
        # -4 is for border fixes
        
        if self.flags() & gtk.REALIZED:
            self.window.move_resize(*allocation)

    
    def do_enter_notify_event(self, event):
        """
        Mouse entered the widget.
        """
        self.queue_draw()
    
    
    def do_leave_notify_event(self, event):
        """
        Mouse left the widget."
        """
        leaving_coords = event.get_coords()
        if not (2 < leaving_coords[0] < self.allocation[2] - 2) or \
        not (2 < leaving_coords[1] < self.allocation[3]):
            self.hovering = -1
        
        self.queue_draw()

    
    def do_button_press_event(self, event):
        """
        Mouse button pressed.
        """
        print "pressing, ", event.time
        
        self.grab_focus()
        
        if event.type == 5 and self.hovering != -1: # double click on a cell
            if not self.zoom:
                self.month = self.hovering + 1
            elif self.zoom == 1:
                self.day = self.hovering + 1
            elif self.zoom == 2:
                self.hour = self.hovering
            else: # already at max zoom level
                return
            
            self.zoom_in()     

    
    def do_button_release_event(self, event):
        """
        Mouse button released.
        """
        print "releasing, ", event.time
        
        if not self.hovering == -1:
            print "clicked on cell:", self.hovering
            
            ## To be Removed
            if self.evt_popup:
                win_origin = self.window.get_origin()
                
                try:
                    self.aaa.close()
                except:
                    pass
                
                self.aaa = TL_EventPopup(self, win_origin[0] + \
                                         (self.hovering*self.__dx) + 3, 
                                         win_origin[1] + self.height, 
                                         self.colors_evt[self.colors[self.hovering]],
                                         self.title_by_zoom(True),
                                         self.title_by_zoom(True), self.zoom,
                                         self.hovering)
                self.aaa.show_all()
            ## End

    
    def do_focus_in_event(self, event):
        """
        Keyboard entered the widget.
        """
        self.__focus = True
        self.queue_draw()

    
    def do_focus_out_event(self, event):
        """
        Keyboard left the widget.
        """
        self.__focus = False
        self.queue_draw()

    
    def do_motion_notify_event(self, event):
        """
        Mouse motion detection.
        """
        if not 2 <= event.y <= self.allocation[3] - 2:
            if self.hovering != -1:
                self.hovering = -1
                self.queue_draw()
            return
        
        prev_status = self.hovering
        hcell = self.__hovering_cell(event.x)
        
        if hcell != -1:
            self.hovering = hcell
        else:
            self.hovering = -1
        
        if prev_status != self.hovering:
            self.queue_draw()

    
    def do_expose_event(self, event):
        """
        Draw upon request.
        """
        cr = self.window.cairo_create()
        cr.rectangle(*event.area)
        cr.clip()
        
        if self.draw_border and self.__focus:
            self.__draw_border_box(cr)
        
        rel_x = 2
        
        for i in xrange(self.scale):
            rel_x = self.__draw_cell(cr, i, rel_x)
        
        if self.draw_backshadow:
            self.__draw_backshadow(cr)


    ##
    # Private methods follows.
    
    def __update_date(self, event):
        """
        This is called on date change and range change request.
        """
        if self.year < self.year_range[0]:
            self.year = self.year_range[0]
        elif self.year > self.year_range[1]:
            self.year = self.year_range[1]
        
        self.hovering = -1
        self.queue_resize()
        self.queue_draw()

    
    def __set_hover(self, hover):
        """
        Set hover state.
        """
        self.__hover = hover


    def __get_cell_mwidth(self):
        """
        Gets the minimal width for a cell.
        """
        return self.__cell_width
    
    
    def __set_cell_mwidth(self, cell_width):
        """
        Sets the minimal width for a cell
        """
        self.__cell_width = cell_width


    def __get_cell_mheight(self):
        """
        Gets the minimal height for a cell.
        """
        return self.__cell_height
    
    
    def __set_cell_mheight(self, cell_height):
        """
        Sets the minimal width for a cell
        """
        self.__cell_height = cell_height


    def __hovering_cell(self, x):
        """
        Check if mouse is hovering a cell, and detect wich one
        is being hovered.
        """
        if 2 <= x <= self.allocation[2] - 2:
            return  int((x - 2)/ self.__dx)
        else:
            return -1


    def __draw_border_box(self, cr):
        """
        Draws a rectangle box, shown when widget is focused.
        """
        cr.save()
        cr.set_line_width(0.2)
        
        cr.set_dash([1, 1], 0)
        cr.new_sub_path()
        cr.rectangle(0, 0, self.allocation[2], self.allocation[3])
        
        cr.close_path()
        cr.stroke()
        cr.restore()
    
    
    def __draw_backshadow(self, cr):
        """
        Draws back shadow.
        """
        cr.rectangle(2, self.height - 10, self.allocation[2] - 4, 8)
        
        pat = cairo.LinearGradient(0, self.height - 10, 0, self.height - 2)
        
        pat.add_color_stop_rgba(0, *tl_colors["shadow_from"])
        pat.add_color_stop_rgba(1, *tl_colors["shadow_to"])
        
        cr.set_source(pat)
        cr.fill()
    

    def __draw_highlight(self, cr, text, rel_x):
        """
        Draw highlight text on top of a cell.
        """
        cr.save()
        cr.select_font_face ("Sans", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        
        font_size = (self.min_font_size)
        if self.__dx > 2 * self.min_cell_width:
            font_size += 2
        
        cr.set_font_size(font_size)
        
        ct_point = self.center_text(cr, text, self.__dx)
        cr.move_to(rel_x + ct_point[0], font_size + 1.5)
        cr.show_text(text)
        cr.restore()
    
    
    def __write_cell_info(self, cr, x_pos, color, hovering, highlight,
                          show_number, info):
        """
        Write something on a cell.
        """
        cr.save()
        cr.select_font_face ("Sans", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_BOLD)
        cr.set_font_size(self.min_font_size + 4)
        
        if too_dark(self.colors_evt[color], level=635):
            cr.set_source_rgba(*tl_colors["cell_fg_hover"])
        else:
            cr.set_source_rgba(0, 0, 0)
        
        ct_point = self.center_text(cr, "%s" % info, self.__dx)
        cr.move_to(x_pos + ct_point[0], ct_point[1])
        
        if show_number or hovering or self.show_every_date:
            cr.show_text("%s" % info)
        if highlight:
            self.__draw_highlight(cr, self.get_highlight_for(info),  x_pos)
        
        cr.restore()
    
    
    def __pre_drawing_cell(self, number):
        """
        Setup cell for drawing.
        """
        setup = { }
        
        # cell being hovered ?
        if (self.hovering == number) and True or False:
            setup["extra_width"] = 1
            setup["line_width"] = 2.5
            setup["hover_draw"] = True
        else:
            setup["extra_width"] = 0
            setup["line_width"] = 1
            setup["hover_draw"] = False
        
        # get color for current cell
        setup["color"] = self.colors[number] 
            
        # adjust number display based on zoom level
        setup["number"] = self.adjust_number_display(number) 
        
        # highlight info for current cell
        hl = self.get_highlight()
        hl_month = self.get_highlight(1)
        if setup["number"] in hl:
            setup["hl"] = True
            setup["show_no"] = True
            setup["top_dist"] = 0
        else:
            setup["hl"] = False
            setup["top_dist"] = 10
            if setup["number"] in hl_month and self.zoom == 1:
                setup["show_no"] = True
            else:
                setup["show_no"] = False
        
        # setup finished.
        return setup


    def __draw_cell(self, cr, number, rel_x):
        """
        Draws a cell.
        """
        # cell setup
        setup = self.__pre_drawing_cell(number)
        
        #
        # draw and paint the cell
        
        cr.save()
        cr.rectangle(rel_x + setup["extra_width"], 2 + setup["top_dist"],
                     self.__dx - setup["extra_width"] * 2,
                     self.height - setup["top_dist"] - 4)
        
        # hovering and light is set
        if setup["hover_draw"] and self.light_on_hover:
            pat = cairo.LinearGradient(rel_x + (self.__dx/2), setup["top_dist"],
                                       rel_x + (self.__dx / 2),
                                       20 + (setup["top_dist"]/2))
            pat.add_color_stop_rgba(1, *self.colors_evt[setup["color"]])
            pat.add_color_stop_rgba(0, 1, 1, 1, 1)
            cr.set_source(pat)
        # light or hovering not set
        else:
            cr.set_source_rgba(*self.colors_evt[setup["color"]])
        
        if self.cell_border:
            cr.fill_preserve()
        else:
            cr.fill()
        cr.restore()
        
        if self.cell_border:
            cr.set_line_width(setup["line_width"])
            cr.stroke()
        
        # write date number and highlight text
        self.__write_cell_info(cr, rel_x, setup["color"], setup["hover_draw"],
                               setup["hl"], setup["show_no"], setup["number"])
        
        
        return (rel_x + self.__dx)

    
    # Properties
    show_every_date = property(get_show_all_dates, set_show_all_dates)
    evt_popup = property(get_evt_popup, set_evt_popup)
    cell_border = property(get_cell_border, set_cell_border)
    light_on_hover = property(get_light_hover, set_light_hover)
    draw_backshadow = property(get_draw_bshadow, set_draw_bshadow)
    fixed_height = property(get_fixed_height, set_fixed_height)
    height = property(get_height)
    min_font_size = property(get_font_size, set_font_size)
    min_cell_width = property(__get_cell_mwidth, __set_cell_mwidth)
    min_cell_height = property(__get_cell_mheight, __set_cell_mheight)
    hovering = property(get_hover, __set_hover)        


gobject.type_register(TL)
